/*
Copyright (c) 2005, David M Howard (daveh at dmh2000.com)
All rights reserved.

This product is licensed for use and distribution under the BSD Open Source License.
see the file COPYING for more details.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, 
OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT 
OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 
OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 
OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 
EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 

*/

/**
 * nmeap01.c
 * nmeap gps data parser
 * 
 * see the file COPYING for terms of the license
*/


#include <string.h>
#include "nmeap.h"

/* this only works if you are sure you have an upper case hex digit */
#define CHAR_TO_HEX(ch)     ((ch) >= '0' && (ch) <= '9' ? (ch) - '0' : (ch) >= 'A' && (ch) <= 'F' ? (ch) - ('A' - 10) : 0)
#define assert              (void)

/* forward references */
int nmeap_tokenize(nmeap_context_t *context);
int nmeap_process(nmeap_context_t *context);
int nmeap_parse(nmeap_context_t *context, unsigned char ch);
int nmeap_parse_buffer(nmeap_context_t *context, const unsigned char *buffer, int *length);

#if 1
#include <ctype.h>

#else
#define _U  0x01    /* upper */
#define _L  0x02    /* lower */
#define _D  0x04    /* digit */
#define _C  0x08    /* cntrl */
#define _P  0x10    /* punct */
#define _S  0x20    /* white space (space/lf/tab) */
#define _X  0x40    /* hex digit */
#define _SP 0x80    /* hard space (0x20) */

unsigned char _ctype[] = 
{
    _C,_C,_C,_C,_C,_C,_C,_C,                                /* 0-7 */
    _C,_C|_S,_C|_S,_C|_S,_C|_S,_C|_S,_C,_C,                 /* 8-15 */
    _C,_C,_C,_C,_C,_C,_C,_C,                                /* 16-23 */
    _C,_C,_C,_C,_C,_C,_C,_C,                                /* 24-31 */
    _S|_SP,_P,_P,_P,_P,_P,_P,_P,                            /* 32-39 */
    _P,_P,_P,_P,_P,_P,_P,_P,                                /* 40-47 */
    _D,_D,_D,_D,_D,_D,_D,_D,                                /* 48-55 */
    _D,_D,_P,_P,_P,_P,_P,_P,                                /* 56-63 */
    _P,_U|_X,_U|_X,_U|_X,_U|_X,_U|_X,_U|_X,_U,              /* 64-71 */
    _U,_U,_U,_U,_U,_U,_U,_U,                                /* 72-79 */
    _U,_U,_U,_U,_U,_U,_U,_U,                                /* 80-87 */
    _U,_U,_U,_P,_P,_P,_P,_P,                                /* 88-95 */
    _P,_L|_X,_L|_X,_L|_X,_L|_X,_L|_X,_L|_X,_L,              /* 96-103 */
    _L,_L,_L,_L,_L,_L,_L,_L,                                /* 104-111 */
    _L,_L,_L,_L,_L,_L,_L,_L,                                /* 112-119 */
    _L,_L,_L,_P,_P,_P,_P,_C,                                /* 120-127 */
    0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,                        /* 128-143 */
    0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,                        /* 144-159 */
    _S|_SP,_P,_P,_P,_P,_P,_P,_P,_P,_P,_P,_P,_P,_P,_P,_P,    /* 160-175 */
    _P,_P,_P,_P,_P,_P,_P,_P,_P,_P,_P,_P,_P,_P,_P,_P,        /* 176-191 */
    _U,_U,_U,_U,_U,_U,_U,_U,_U,_U,_U,_U,_U,_U,_U,_U,        /* 192-207 */
    _U,_U,_U,_U,_U,_U,_U,_P,_U,_U,_U,_U,_U,_U,_U,_L,        /* 208-223 */
    _L,_L,_L,_L,_L,_L,_L,_L,_L,_L,_L,_L,_L,_L,_L,_L,        /* 224-239 */
    _L,_L,_L,_L,_L,_L,_L,_P,_L,_L,_L,_L,_L,_L,_L,_L         /* 240-255 */
};

#define __ismask(x) (_ctype[(int)(unsigned char)(x)])

#define isalnum(c)  ((__ismask(c)&(_U|_L|_D)) != 0)
#define isalpha(c)  ((__ismask(c)&(_U|_L)) != 0)
#define iscntrl(c)  ((__ismask(c)&(_C)) != 0)
#define isgraph(c)  ((__ismask(c)&(_P|_U|_L|_D)) != 0)
#define islower(c)  ((__ismask(c)&(_L)) != 0)
#define isprint(c)  ((__ismask(c)&(_P|_U|_L|_D|_SP)) != 0)
#define ispunct(c)  ((__ismask(c)&(_P)) != 0)
#define isspace(c)  ((__ismask(c)&(_S)) != 0)
#define isupper(c)  ((__ismask(c)&(_U)) != 0)
#define isxdigit(c) ((__ismask(c)&(_D|_X)) != 0)
#endif

int nmeap_strncmp(const char *cs, const char *ct, unsigned int count)
{
    signed char __res = 0;

    while (count)
    {
        if ((__res = *cs - *ct++) != 0 || !*cs++)
            break;
        count--;
    }

    return __res;
}

unsigned int nmeap_get_timestamp(const nmeap_date_t *date, const nmeap_time_t *time)
{
    unsigned int year = date->year, month = date->month;
    unsigned int ret;

#define LEAP_YEARS  ((year >> 2) - 15)      // year/4 - year/100 + year/400, year=20xx

    if (year < 2000 || year > 2099)
    {
        year = 2020;
    }

    /* 1..12 -> 3,4,5..1,2 */  
    if (month <= 2)
    {
        month += 12;    /* Puts Feb last since it has leap day */  
        year -= 1;  
    }  
    month -= 2;
  
    ret = LEAP_YEARS + 367 * month / 12 + date->date;
    ret += year * 365 - 719499;         // since 1970-01-01 00:00:00
    ret = ret * 24 + time->hour;
    ret = ret * 60 + time->minute;
    ret = ret * 60 + time->second;

    return ret;
}

/**
 * initialize an NMEA parser
 */
int nmeap_init(nmeap_context_t *context, void *user_data)
{
    assert(context != 0);

    memset(context, 0, sizeof(*context));

    context->user_data = user_data;

    return 0;
}

/**
 * register an NMEA sentence parser
 */
int nmeap_add_parser(nmeap_context_t            *context,
                     const char                 *sentence_name,
                     nmeap_sentence_parser_t    sentence_parser,
                     nmeap_callout_t            sentence_callout,
                     void                       *sentence_data
                    )
{
    nmeap_sentence_t *s = 0;

    /* runtime error */
    assert(context != 0);

    /* sentence capacity overflow */
    if (context->sentence_count >= NMEAP_MAX_SENTENCES)
    {
        return -1;
    }

    /* point at next empty sentence buffer */
    s = &context->sentence[context->sentence_count];

    /* advance sentence data count */
    context->sentence_count++;

    /* clear the sentence data */
    memset(s, 0, sizeof(*s));

    /* name */
    strncpy(s->name, sentence_name, NMEAP_MAX_SENTENCE_NAME_LENGTH);

    /* parser */
    s->parser = sentence_parser;

    /* callout */
    s->callout = sentence_callout;

    /* data */
    s->dataout = sentence_data;

    return 0;
}

/** 
 * tokenize a buffer
 */
int nmeap_tokenize(nmeap_context_t *context)
{
    char *s;
    int   tokens;
    int   state;

    /* first token is header. assume it is there */
    tokens = 0;
    s = context->input;
    context->token[tokens] = s;

    /* get rest of tokens */
    tokens = 1;
    state = 0;
    while ((*s != 0) && (tokens < NMEAP_MAX_TOKENS))
    {
        switch (state)
        {
            case 0:
                /* looking for end of a token */
                if (*s == ',')
                {
                    /* delimit at the comma */
                    *s    = 0;
                    /* new token */
                    state = 1;
                }
                break;

            case 1:
                /* start of next token, might be another comma */
                context->token[tokens++] = s;
                if (*s == ',')
                {
                    /* delimit at the comma */
                    *s    = 0;
                }
                else
                {
                    /* not a comma */
                    state = 0;
                }
                break;

            default:
                state = 0;
                break;
        }

        // next character
        s++;
    }
    return tokens;
}

/**
 * process a sentence 
 */
int nmeap_process(nmeap_context_t *context)
{
    int id = NMEAP_ID_UNKNOWN;
    int i;
    nmeap_sentence_t *s;

#if (defined NMEAP_DEBUG) && (NMEAP_DEBUG != 0)     
    /* copy the input to a debug buffer */
    /* remove debug_input when everything is working. */
    strncpy(context->debug_input, context->input, sizeof(context->debug_input));
#endif

    /* tokenize the input */
    context->tokens = nmeap_tokenize(context);

    /* try to find a matching sentence parser */
    /* this search is O(n). it has a lot of potential for optimization, at the expense of complexity, if you have a lot of sentences */
    /* binary search instead of linear (have to keep sentences in sorted order) O(NlogN) */
    /* OR, when sentences are added, create a TRIE structure to find the names with a constant time search O(5) */
    for (i = 0; i < context->sentence_count; i++)
    {
        s = &context->sentence[i];
        assert(s != 0);

        // "GPGGA" or "BDGGA" or "xxGGA" ...
        if (nmeap_strncmp(&context->input_name[2], s->name, 3) == 0)
        {
            /* found a match, call its parser */
            id = (*context->sentence[i].parser)(context, s);
            if (id > 0)
            {
                break;
            }
        }
    }

    return id;
}

/**
                 +-5-+              +---+
                 v   |              v   |
 +------+       +------+           +------+         +------+         +------+
 |   0  |--$--> |1-hdr |--alnum--> |2-data|----\r-->| 6-LF |---\n--->| done |--> 0
 +------+       +------+           +------+         +------+         +------+
                                    |                 ^
                                    *                 +--------\r-------+
                                    V                                   |
                                 +------+           +------+         +------+
                                 |3-cks |--xdigit-->|4-cks |-xdigit->| 5-CR |
                                 +------+           +------+         +------+
                                 
return to start conditions:
1. buffer overflow
2. invalid character for state

checksum calculation
two hex digits represent the XOR of all characters between, but not
including, the "$" and "*".  A checksum is required on some
sentences.

sentence example:   
$GPGGA,061020.00,3456.012345,N,12345.678901,E,1,03,1.6,121.8,M,-13.0,M,,*4A
$GPRMC,061020.00,A,3456.012345,N,12345.678901,E,0.0,0.0,120620,5.0,W,A*27
*/
int nmeap_parse(nmeap_context_t *context, unsigned char ch)
{
    int status = 0;

    /* check for input buffer overrun first to avoid duplicating code in the
    individual states
    */
    if (context->input_count >= (sizeof(context->input) - 1))
    {
        /* input buffer overrun, restart state machine */
        context->input_state = 0;
        /* reset input count */
        context->input_count = 0;
    }

    /* store the byte */
    context->input[context->input_count] = ch;

    /* next buffer position */
    context->input_count++;

    /* run it through the lexical scanner */
    switch (context->input_state)
    {
        /* LOOKING FOR $ */
        case 0:
            if (ch == '$')
            {
                /*look for id */
                context->input_state = 1;
                context->ccks        = 0;
                context->icks        = 0;
            }
            else
            {
                /* header error, start over */
#if (defined NMEAP_DEBUG) && (NMEAP_DEBUG != 0)     
                context->err_hdr++;
#endif
                context->input_state = 0;
                context->input_count = 0;
            }
            break;

            /* LOOKING FOR 5 CHARACTER SENTENCE ID */
        case 1:
            /* allow numbers even though it isn't usually done */
            /* a proprietary id might have a numeral */
            if (isalnum(ch))
            {
                /* store name separately */
                context->input_name[context->input_count - 2] = ch;
                /* checksum */
                context->ccks ^= ch;
                /* end of header? */
                if (context->input_count >= 6)
                {
                    /* yes, get body */
                    context->input_state = 2;
                }
            }
            else
            {
                /* bad character, start over */
#if (defined NMEAP_DEBUG) && (NMEAP_DEBUG != 0)     
                context->err_id++;
#endif
                context->input_state = 0;
                context->input_count = 0;
            }
            break;

            /* LOOKING FOR CR OR CHECKSUM INDICATOR */
        case 2:
            if (ch == '*')
            {
                /* this sentence has a checksum */
                context->input_state = 3;
            }
            else if (ch == '\r')
            {
                /* carriage return, no checksum, force a match */
                context->icks = 0;
                context->ccks = 0;
                context->input_state = 6;
            }
            else
            {
                /* continue accumulating data */
                /* checksum */
                context->ccks ^= ch;
            }
            break;

            /* LOOKING FOR FIRST CHECKSUM CHARACTER */
        case 3:
            /* must be upper case hex digit */
            if (isxdigit(ch) && (ch <= 'F'))
            {
                /* got first checksum byte */
                context->input_state = 4;
                context->icks = CHAR_TO_HEX(ch) << 4;
            }
            else
            {
                /* input error, restart */
#if (defined NMEAP_DEBUG) && (NMEAP_DEBUG != 0)     
                context->err_cks++;
#endif                
                context->input_state = 0;
                context->input_count = 0;
            }
            break;

            /* LOOKING FOR SECOND CHECKSUM CHARACTER */
        case 4:
            /* must be upper case hex digit */
            if (isxdigit(ch) && (ch <= 'F'))
            {
                /* got second checksum byte */
                context->input_state = 5;
                context->icks += CHAR_TO_HEX(ch);
            }
            else
            {
                /* input error, restart */
#if (defined NMEAP_DEBUG) && (NMEAP_DEBUG != 0)     
                context->err_cks++;
#endif
                context->input_state = 0;
                context->input_count = 0;
            }
            break;

            /* LOOKING FOR CR */
        case 5:
            if (ch == '\r')
            {
                /* carriage return */
                context->input_state = 6;
            }
            else
            {
                /* input error, restart */
#if (defined NMEAP_DEBUG) && (NMEAP_DEBUG != 0)     
                context->err_crl++;
#endif
                context->input_state = 0;
                context->input_count = 0;
            }
            break;

            /* LOOKING FOR LF */
        case 6:
            if (ch == '\n')
            {
                /* linefeed, line complete */

                /* delimit buffer */
                context->input[context->input_count] = 0;

                /* if the checksums match, process the sentence */
                if (context->ccks == context->icks)
                {
                    /* process */
                    status = nmeap_process(context);

#if (defined NMEAP_DEBUG) && (NMEAP_DEBUG != 0)     
                    /* count good messages */
                    context->msgs++;
                }
                else
                {
                    /* count checksum errors */
                    context->err_cks++;
#endif
                }

                /* restart next time */
                context->input_state = 0;
                context->input_count = 0;
            }
            else
            {
                /* input error, restart */
#if (defined NMEAP_DEBUG) && (NMEAP_DEBUG != 0)     
                context->err_crl++;
#endif
                context->input_state = 0;
                context->input_count = 0;
            }
            break;

        default:
#if (defined NMEAP_DEBUG) && (NMEAP_DEBUG != 0)     
            context->err_unk++;
#endif
            context->input_state = 0;
            context->input_count = 0;
            break;
    }

    return status;
}

/** 
 * parse a buffer of nmea data
 */
int nmeap_parse_buffer(nmeap_context_t *context, const unsigned char *buffer, int *length)
{
    int  i;
    int  status;
    int  rem;
    int  tlen;

    tlen   = *length;
    rem    = *length;
    status = 0;
    /* for each byte in the buffer */
    for (i = 0; i < tlen; i++)
    {
        /* decrement remaining byte count */
        rem--;
        /* parse the byte */
        status = nmeap_parse(context, buffer[i]);
        if (status != 0)
        {
            /* message found or error */
            break;
        }
    }

    /* return remaining byte count */
    *length = rem;

    return status;
}



